// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/gateio/order_v4/parser.h"

#include <sstream>

#include <boost/algorithm/string.hpp>

#include "coin/proto/coin_query.pb.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/gateio/api_order/native_private_client.h"
#include "coin2/exchange/gateio/symbology/currency.h"
#include "coin2/exchange/util/json_util.h"
namespace coin2::exchange::gateio::order_v4 {

namespace {

using coin::proto::FillType;
using coin::proto::TransferRequestProto;
using coin2::exchange::gateio::api_order::GateioOrderSubTopicId;
using coin2::exchange::gateio::symbology::GateioCurrency;

inline bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("channel") && json.HasMember("event") && json.HasMember("result")) {
    return (json["channel"] == "spot.orders") && (json["event"] == "update") &&
           json["result"].IsArray() && (json["result"].GetArray().Size() > 0);
  }
  return false;
}

inline bool IsTradeUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("channel") && json.HasMember("event") && json.HasMember("result")) {
    return (json["channel"] == "spot.usertrades") && (json["event"] == "update") &&
           json["result"].IsArray() && (json["result"].GetArray().Size() > 0);
  }
  return false;
}

inline bool IsBalanceUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("channel") && json.HasMember("event") && json.HasMember("result")) {
    return (json["channel"] == "spot.balances") && (json["event"] == "update") &&
           json["result"].IsArray() && (json["result"].GetArray().Size() > 0);
  }
  return false;
}

}  // namespace

auto GateioParser::native_currencies() const -> const CurrencyPairs& {
  static CurrencyPairs currency_pairs = [&]() -> auto {
    CurrencyPairs tmp;
    auto currency_set = symcache_->GetCurrencies();
    for (auto& currency : currency_set) {
      const auto& native_currency = currency->native_currency();
      tmp.emplace(std::make_pair(boost::to_lower_copy(native_currency), native_currency));
    }
    return tmp;
  }();
  return currency_pairs;
}

int64_t GateioParser::GetTimestamp(const std::string& time_str) const {
  return stoll(time_str) * 1e9;
}

bool GateioParser::IsGateioErrMessage(const JsonValue& data) const {
  return data.IsObject() && data.HasMember("label");
}

std::optional<std::string> GateioParser::GetExchangeErrorCode(const JsonValue& data) const {
  if (!data.HasMember("label")) {
    return std::nullopt;
  }

  return data["label"].GetString();
}

std::optional<std::string> GateioParser::GetExchangeErrorMsg(const JsonValue& data) const {
  if (data.HasMember("message")) {
    return data["message"].GetString();
  }

  if (data.HasMember("detail")) {
    return data["detail"].GetString();
  }

  return std::nullopt;
}

auto GateioParser::GetErrorCode(const std::string& error_code) const -> OrderErrorCode {
  if (error_code == "BALANCE_NOT_ENOUGH") {
    return OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND;
  }

  if (error_code == "ORDER_NOT_FOUND") {
    return OrderErrorCode::ORDER_NOT_FOUND;
  }

  return OrderErrorCode::ORDER_UNKNOWN_ERROR;
}

int64_t GateioParser::GetProcOrderId(const JsonValue& data) const {
  if (!data.HasMember("text")) {
    return 0;
  }
  std::string_view proc_order_id = data["text"].GetString();
  if (boost::starts_with(proc_order_id, "t-")) {
    proc_order_id = proc_order_id.substr(2, proc_order_id.size() - 2);
  } else {
    LOG(ERROR) << "[GetProcOrderId] Invalid test format\n" << JsonToString(data);
  }
  return stoll(proc_order_id);
}

auto GateioParser::GetOrderSide(const std::string& side) const -> OrderSide {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline bool GateioParser::RawInfoParseJsonAndCheck(JsonDocument& document, std::string func_name) {
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[" << func_name << "]"
               << "Parsing error\n"
               << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return false;
  }

  if (document.IsObject() && IsGateioErrMessage(document)) {
    LOG(ERROR) << "[" << func_name << "]"
               << "Error message\n"
               << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_MSG_TYPE);
    return false;
  }

  return true;
}

void GateioParser::ParseRestDeposit() {
  /*
  [
    {
      "id": "d76060021",
      "currency": "USDT",
      "address": "TLVy1ZHxnK2BFzgQ3iKELfwGTZNKRq8JfR",
      "amount": "250000",
      "txid": "fc7863572ee6b160fff59e10465d20a12ca70a54929e977cc3e61670073a070e",
      "chain": "TRX",
      "timestamp": "1662104654",
      "status": "DONE",
      "memo": ""
    }
  ]
  */
  JsonDocument document;
  if (!RawInfoParseJsonAndCheck(document, __func__)) {
    return;
  }
  if (!document.IsArray()) {
    result_.set_error_code(OrderErrorCode::INVALID_MSG_TYPE);
    LOG(ERROR) << "[ParseRestDeposit] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    const static std::vector<std::string> fields =
        {"currency", "amount", "address", "timestamp", "status"};
    if (!JsonFieldCheck(data, fields)) {
      LOG(ERROR) << "[ParseRestDeposit] Invalid Deposit format" << JsonToString(data);
      continue;
    }

    std::string transfer_id;
    if (!data.HasMember("txid") || data["txid"].IsNull() ||
        (std::string(data["txid"].GetString()) == "")) {
      if (!data.HasMember("id")) {
        LOG(ERROR) << "[ParseRestDeposit] Invalid Deposit format" << JsonToString(data);
        continue;
      }
      LOG(INFO) << "[ParseRestDeposit] txid is null, use id for external transfer id"
                << JsonToString(data);
      transfer_id = data["id"].GetString();
    } else {
      transfer_id = data["txid"].GetString();
    }

    auto proto_status = TransferRequestProto::TRANSFER_ACCEPTED;
    std::string status = data["status"].GetString();

    if (!transfer_first_record_) {
      if (transfer_deposit_map_[transfer_id] == status) {
        continue;  // Filtering Duplicates
      }
    }

    transfer_deposit_map_.insert_or_assign(transfer_id, status);
    if (status == "REQUEST") {
      proto_status = TransferRequestProto::TRANSFER_SUBMITTED;
    } else if (
        status == "MANUAL" || status == "BCODE" || status == "EXTPEND" || status == "VERIFY" ||
        status == "PROCES" || status == "PEND" || status == "DMOVE" || status == "SPLITPEND") {
      proto_status = TransferRequestProto::TRANSFER_ACCEPTED;
    } else if (status == "DONE") {
      proto_status = TransferRequestProto::TRANSFER_CONFIRMED;
    } else if (status == "CANCEL") {
      proto_status = TransferRequestProto::CANCEL_CONFIRMED;
    } else if (status == "FAIL" || status == "INVALID") {
      proto_status = TransferRequestProto::TRANSFER_REJECTED;
    } else {
      LOG(ERROR) << "[ParseRestDeposit] Unknown Deposit status \n" << JsonToString(data);
      continue;
    }

    int64_t create_timestamp = stoll(data["timestamp"].GetString()) * 1e9;
    auto currency =
        GateioCurrency::FromNativeStrImpl(data["currency"].GetString(), create_timestamp);
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    currency_transfer->set_type(TransferRequestProto::DEPOSIT);
    currency_transfer->set_status(proto_status);
    currency_transfer->set_external_transfer_id(transfer_id);
    currency_transfer->set_qty(stod(data["amount"].GetString()));
    currency_transfer->set_from_address((data["address"].GetString()));
    currency_transfer->set_create_timestamp(create_timestamp);
    currency_transfer_bundle_found = true;
  }

  if (!transfer_first_record_) {
    if (!transfer_first_record_) {
      if (!is_transfer_deposit_initialized_) {
        is_transfer_deposit_initialized_ = true;
        LOG(INFO) << "[ParseRestDeposit] Deposit ignored: " << transfer_deposit_map_.size();
        return;
      }
    }
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestDeposit] Deposit parsed: "
              << currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }

  return;
}

void GateioParser::ParseRestWithdraw() {
  /*
  [
    {
        "id": "210496",
        "timestamp": "1542000000",
        "currency": "USDT",
        "address": "1HkxtBAMrA3tP5ENnYY2CZortjZvFDH5Cs",
        "txid": "128988928203223323290",
        "amount": "222.61",
        "memo": "",
        "status": "DONE",
        "chain": "TRX"
      }
  ]
  */

  JsonDocument document;
  if (!RawInfoParseJsonAndCheck(document, "ParseRestWithdraw")) {
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestWithdraw] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    const static std::vector<std::string> fields =
        {"timestamp", "amount", "currency", "status", "address"};
    if (!JsonFieldCheck(data, fields)) {
      LOG(ERROR) << "[ParseRestWithdraw] Invalid Withdraw format" << JsonToString(data);
      continue;
    }

    std::string transfer_id;
    if (!data.HasMember("txid") || data["txid"].IsNull() ||
        (std::string(data["txid"].GetString()) == "")) {
      if (!data.HasMember("id")) {
        LOG(ERROR) << "[ParseRestWithdraw] Invalid Deposit format" << JsonToString(data);
        continue;
      }
      LOG(INFO) << "[ParseRestWithdraw] txid is null, use id for external transfer id"
                << JsonToString(data);
      transfer_id = data["id"].GetString();
    } else {
      transfer_id = data["txid"].GetString();
    }

    auto proto_status = TransferRequestProto::TRANSFER_ACCEPTED;
    std::string status = data["status"].GetString();

    if (!transfer_first_record_) {
      if (transfer_withdrawal_map_[transfer_id] == status) {
        continue;  // Filtering Duplicates
      }
    }

    transfer_withdrawal_map_.insert_or_assign(transfer_id, status);
    if (status == "REQUEST") {
      proto_status = TransferRequestProto::TRANSFER_SUBMITTED;
    } else if (
        status == "MANUAL" || status == "BCODE" || status == "EXTPEND" || status == "VERIFY" ||
        status == "PROCES" || status == "PEND" || status == "DMOVE" || status == "SPLITPEND") {
      proto_status = TransferRequestProto::TRANSFER_ACCEPTED;
    } else if (status == "DONE") {
      proto_status = TransferRequestProto::TRANSFER_CONFIRMED;
    } else if (status == "CANCEL") {
      proto_status = TransferRequestProto::CANCEL_CONFIRMED;
    } else if (status == "FAIL" || status == "INVALID") {
      proto_status = TransferRequestProto::TRANSFER_REJECTED;
    } else {
      LOG(ERROR) << "[ParseRestWithdraw] Unknown Withdraw status \n" << JsonToString(data);
      continue;
    }

    int64_t create_timestamp = stoll(data["timestamp"].GetString()) * 1e9;
    auto currency =
        GateioCurrency::FromNativeStrImpl(data["currency"].GetString(), create_timestamp);
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_type(TransferRequestProto::WITHDRAW);
    currency_transfer->set_create_timestamp(create_timestamp);
    currency_transfer->set_currency(currency->currency());
    currency_transfer->set_qty(stod(data["amount"].GetString()));
    currency_transfer->set_to_address((data["address"].GetString()));
    currency_transfer->set_status(proto_status);
    currency_transfer->set_external_transfer_id(transfer_id);
    currency_transfer_bundle_found = true;
  }

  if (!transfer_first_record_) {
    if (is_transfer_withdrawal_initialized_) {
      is_transfer_withdrawal_initialized_ = true;
      LOG(INFO) << "[ParseRestWithdraw] Deposit ignored: " << transfer_withdrawal_map_.size();
      return;
    }
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestWithdraw] Deposit parsed: "
              << currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }

  return;
}

void GateioParser::ParseRestTransfers() {
  /*
  [
    {
      "uid": 5907529,
      "amount": "29199.571801",
      "sub_account_type": "spot",
      "source": "web",
      "timest": "1662104937",
      "direction": "from",
      "currency": "USDT",
      "sub_account": "11751193"
    }
  ]
  */
  JsonDocument document;
  if (!RawInfoParseJsonAndCheck(document, __func__)) {
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestTransfers] Invalid data format\n" << JsonToString(document);
    return;
  }
  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    const static std::vector<std::string> fields =
        {"currency", "amount", "direction", "sub_account_type", "timest", "uid", "sub_account"};
    if (!JsonFieldCheck(data, fields)) {
      LOG(ERROR) << "[ParseRestTransfers] Invalid transfer format" << JsonToString(data);
      continue;
    }

    std::string direction = data["direction"].GetString();
    std::string account_type = data["sub_account_type"].GetString();
    std::string timest = data["timest"].GetString();
    std::string key = absl::StrCat(account_type, timest);

    if (!transfer_first_record_) {
      if (transfer_subaccount_map_[key] == "done") {
        continue;
      }
    }
    transfer_subaccount_map_.insert_or_assign(key, "done");
    auto transfer_type = TransferRequestProto::DEPOSIT_SUBACCOUNT;
    if (direction == "to") {
      transfer_type = TransferRequestProto::WITHDRAW_SUBACCOUNT;
    } else if (direction == "from") {
      transfer_type = TransferRequestProto::DEPOSIT_SUBACCOUNT;
    } else {
      LOG(ERROR) << "[ParseRestTransfers] Unknown Transfers direction " << JsonToString(data)
                 << "\n";
      continue;
    }

    int64_t create_timestamp = stoll(timest) * 1e9;
    auto currency =
        GateioCurrency::FromNativeStrImpl(data["currency"].GetString(), create_timestamp);
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
    currency_transfer->set_type(transfer_type);
    currency_transfer->set_create_timestamp(create_timestamp);
    currency_transfer->set_qty(stod(data["amount"].GetString()));

    if (transfer_type == TransferRequestProto::WITHDRAW_SUBACCOUNT) {
      currency_transfer->set_from_address(data["uid"].GetString());
      currency_transfer->set_to_address(data["sub_account"].GetString());
    } else {
      currency_transfer->set_from_address(data["sub_account"].GetString());
      currency_transfer->set_to_address(data["uid"].GetString());
    }
    currency_transfer_bundle_found = true;
  }

  if (!transfer_first_record_) {
    if (!is_transfer_subaccount_initialized_) {
      is_transfer_subaccount_initialized_ = true;
      LOG(INFO) << "[ParseRestTransfer] Transfer deposit subaccount ignored: "
                << transfer_subaccount_map_.size();
      return;
    }
  }

  if (currency_transfer_bundle_found) {
    LOG(INFO) << "[ParseRestTransfers] Transfers parsed: "
              << currency_transfer_bundle->each_transfer_size();
    result_.set_type(MsgType::TRANSFER);
  }
  return;
}

auto GateioParser::GetOrderSide(int64_t type) const -> OrderSide {
  if (type == 2) {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (type == 1) {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

auto GateioParser::GetOrderType(const std::string& type) const -> OrderType {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

auto GateioParser::GetOrderType(int64_t orderType) const -> OrderType {
  if (orderType == 1) {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (orderType == 2) {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void GateioParser::GetOrderStatus(
    const JsonValue& data,
    ProductOrderElement* product_order_element) {
  std::string_view status = data["status"].GetString();
  if ("open" == status) {
    product_order_element->set_is_live(true);
  } else if ("cancelled" == status) {
    product_order_element->set_is_live(false);
    product_order_element->set_cancel_confirmed(true);
  } else if ("closed" == status) {
    product_order_element->set_is_live(false);
    product_order_element->set_fully_filled(true);
  }
}

void GateioParser::SetOrderStatus(
    const JsonValue& data,
    ProductOrderElement* product_order_element) {
  std::string_view event = data["event"].GetString();
  if (event == "put") {
    product_order_element->set_is_live(true);
  } else if (event == "update") {
    double left = stod(data["left"].GetString());
    if (left <= 1e-6) {
      product_order_element->set_is_live(false);
      product_order_element->set_fully_filled(true);
    } else {
      product_order_element->set_is_live(true);
      product_order_element->set_fully_filled(false);
    }
  } else if (event == "finish") {
    double left = stod(data["left"].GetString());
    if (left <= 1e-6) {
      product_order_element->set_is_live(false);
      product_order_element->set_fully_filled(true);
    } else {
      product_order_element->set_is_live(false);
      product_order_element->set_fully_filled(false);
      product_order_element->set_cancel_confirmed(true);
    }
  }
}

void GateioParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseSubmitResponse] failed [" << order_context->proc_order_id() << "], "
               << "AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[ParseSubmitResponse] failed [" << order_context->proc_order_id() << "], "
               << "API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() != 201) {
    LOG(ERROR) << "[ParseSubmitResponse] failed [" << order_context->proc_order_id() << "], "
               << "status code: " << http_context->res.result_int()
               << ", payload: " << http_context->res.body().data();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseSubmitResponse] failed [" << order_context->proc_order_id() << "], "
               << "Parse Error\n"
               << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (doc.IsObject() && IsGateioErrMessage(doc)) {
    LOG(ERROR) << "[ParseSubmitResponse] failed [" << order_context->proc_order_id() << "], "
               << JsonToString(doc);
    auto exchange_error_code = GetExchangeErrorCode(doc);
    auto exchange_error_msg = GetExchangeErrorMsg(doc);
    auto error_code = GetErrorCode(exchange_error_code.value());

    resp->set_error_code(error_code);
    resp->set_exchange_error_code(exchange_error_code.value());
    if (exchange_error_msg) {
      resp->set_exchange_error_msg(exchange_error_msg.value());
    }
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  if (doc.HasMember("id")) {
    resp->set_exchange_order_id(doc["id"].GetString());
  } else {
    LOG(ERROR) << "[ParseSubmitResponse] Invalid data format\n" << JsonToString(doc);
  }
}

void GateioParser::ParseBatchSubmitResponse() {
  /*
  [
      {
          "text":"t-1687946268076778091",
          "succeeded":true,
          "label":"",
          "message":"",
          "id":"357001735808",
          "create_time":"1687947549",
          "update_time":"1687947549",
          "create_time_ms":1687947549380,
          "update_time_ms":1687947549380,
          "status":"open",
          "currency_pair":"BTC_USDT",
          "type":"limit",
          "account":"spot",
          "side":"sell",
          "amount":"0.00004",
          "price":"30325.1",
          "time_in_force":"poc",
          "iceberg":"0",
          "left":"0.00004",
          "fill_price":"0",
          "filled_total":"0",
          "fee":"0",
          "fee_currency":"USDT",
          "point_fee":"0",
          "gt_fee":"0",
          "gt_discount":true,
          "rebated_fee":"0",
          "rebated_fee_currency":"BTC",
          "finish_as":"open"
      }
  ]
  */

  const auto& oc_list = msg_->oc_list;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::MULTI_SUBMIT_RESPONSE);
  auto* response = result_.mutable_multi_submit_response();
  response->Clear();

  std::unordered_map<int64_t, SubmitOrderResponse*> proc_order_id_to_response_map;
  for (const auto* order_context : oc_list) {
    auto* resp = response->add_each_submit_response();

    resp->set_symbol(order_context->order_spec().product);
    resp->set_proc_order_id(order_context->proc_order_id());
    resp->set_http_status(http_context->res.result_int());
    resp->set_timestamp(msg_->timestamp);
    resp->set_success(false);

    CHECK(proc_order_id_to_response_map.count(order_context->proc_order_id()) == 0)
        << "proc_order_id duplicate proc_order_id:" << order_context->proc_order_id();
    proc_order_id_to_response_map[order_context->proc_order_id()] = resp;
  }

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[" << __func__ << "] AsioHttpContextStatus Error";
    for (auto& resp : *(response->mutable_each_submit_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    }
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[" << __func__ << "] API_LIMIT_ERROR";
    for (auto& resp : *(response->mutable_each_submit_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    }
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json) || !json.IsArray()) {
    LOG(ERROR) << "[" << __func__ << "] Parse Error\n" << http_context->res;
    for (auto& resp : *(response->mutable_each_submit_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    }
    return;
  }

  const auto& array = json.GetArray();

  CHECK_EQ(proc_order_id_to_response_map.size(), array.Size())
      << "Batch submit response size not equal to request size response:" << JsonToString(json)
      << " request:" << response->DebugString();

  for (const auto& data : array) {
    int64_t client_order_id = GetProcOrderId(data);

    CHECK(proc_order_id_to_response_map.count(client_order_id) != 0)
        << "unknown client order id found client_order_id:" << client_order_id;

    auto* resp = proc_order_id_to_response_map[client_order_id];

    auto exchange_error_code = GetExchangeErrorCode(data);
    auto exchange_error_msg = GetExchangeErrorMsg(data);
    if (exchange_error_code && !exchange_error_code.value().empty() && exchange_error_msg &&
        !exchange_error_msg.value().empty()) {
      LOG(ERROR) << "[" << __func__ << "] failed [" << client_order_id << "], "
                 << JsonToString(data);
      auto error_code = GetErrorCode(exchange_error_code.value());

      resp->set_error_code(error_code);
      resp->set_exchange_error_code(exchange_error_code.value());
      resp->set_exchange_error_msg(exchange_error_msg.value());
      continue;
    }

    if (!data.HasMember("id")) {
      LOG(ERROR) << "[" << __func__ << "] Invalid data format\n" << JsonToString(data);
      resp->set_error_code(OrderErrorCode::INVALID_JSON);
      continue;
    }

    resp->set_exchange_order_id(data["id"].GetString());
    resp->set_success(true);
  }
}

void GateioParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseCancelResponse] failed [" << order_context->proc_order_id() << "], "
               << "AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[ParseCancelResponse] failed [" << order_context->proc_order_id() << "], "
               << "API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() != 200) {
    LOG(ERROR) << "[ParseCancelResponse] failed [" << order_context->proc_order_id() << "], "
               << "status code: " << http_context->res.result_int()
               << ", payload: " << http_context->res.body().data();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseCancelResponse] failed [" << order_context->proc_order_id() << "], "
               << "Parse Error\n"
               << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (doc.IsObject() && IsGateioErrMessage(doc)) {
    LOG(ERROR) << "[ParseCancelResponse] failed [" << order_context->proc_order_id() << "], "
               << JsonToString(doc);
    auto exchange_error_code = GetExchangeErrorCode(doc);
    auto exchange_error_msg = GetExchangeErrorMsg(doc);
    auto error_code = GetErrorCode(exchange_error_code.value());

    resp->set_error_code(error_code);
    resp->set_exchange_error_code(exchange_error_code.value());
    if (exchange_error_msg) {
      resp->set_exchange_error_msg(exchange_error_msg.value());
    }
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  if (doc.HasMember("id")) {
    resp->set_exchange_order_id(doc["id"].GetString());
  } else {
    LOG(ERROR) << "[ParseCancelResponse] Invalid data format\n" << JsonToString(doc);
  }
  resp->set_cancel_confirmed(true);
}

void GateioParser::ParseBatchCancelResponse() {
  /*
  [
      {
          "currency_pair":"BTC_USDT",
          "id":"373491452752",
          "succeeded":true
      },
      {
          "currency_pair":"SOL_USDT",
          "id":"373491582194",
          "succeeded":false,
          "label":"ORDER_NOT_FOUND",
          "message":"Order not found"
      }
  ]
  */

  const auto& oc_list = msg_->oc_list;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::MULTI_CANCEL_RESPONSE);
  auto* response = result_.mutable_multi_cancel_response();
  response->Clear();

  std::unordered_map<std::string, CancelOrderResponse*> order_id_to_response_map;
  for (const auto* order_context : oc_list) {
    auto* resp = response->add_each_cancel_response();

    resp->set_symbol(order_context->order_spec().product);
    resp->set_proc_order_id(order_context->proc_order_id());
    resp->set_http_status(http_context->res.result_int());
    resp->set_exchange_order_id(order_context->exchange_order_id());
    resp->set_timestamp(msg_->timestamp);
    resp->set_success(false);

    CHECK(order_context->has_exchange_order_id())
        << "Unexpect order_context not have exchange_order_id, please check! order_context:"
        << order_context->ToProto().DebugString();

    CHECK(order_id_to_response_map.count(order_context->exchange_order_id()) == 0)
        << "exchange_order_id duplicate exchange_order_id:" << order_context->exchange_order_id();
    order_id_to_response_map[order_context->exchange_order_id()] = resp;
  }

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[" << __func__ << "] AsioHttpContextStatus Error";
    for (auto& resp : *(response->mutable_each_cancel_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    }
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[" << __func__ << "] API_LIMIT_ERROR";
    for (auto& resp : *(response->mutable_each_cancel_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    }
    return;
  }

  if (http_context->res.result_int() != 200) {
    LOG(ERROR) << "[" << __func__
               << "] http status code != 200 status code: " << http_context->res.result_int()
               << ", payload: " << http_context->res.body().data();
    for (auto& resp : *(response->mutable_each_cancel_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    }
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json) || !json.IsArray()) {
    LOG(ERROR) << "[" << __func__ << "] Parse Error\n" << http_context->res;
    for (auto& resp : *(response->mutable_each_cancel_response())) {
      resp.set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    }
    return;
  }

  const auto& array = json.GetArray();

  CHECK_EQ(order_id_to_response_map.size(), array.Size())
      << "Batch cancel response size not equal to request size msg:" << msg_->DebugString();

  for (const auto& data : array) {
    if (!data.HasMember("id") || !data["id"].IsString() || !data.HasMember("succeeded") ||
        !data["succeeded"].IsBool()) {
      LOG(ERROR) << "[" << __func__ << "] Invalid data format\n" << JsonToString(data);
      continue;
    }

    std::string order_id = data["id"].GetString();

    CHECK(order_id_to_response_map.count(order_id))
        << "unknown order_id:" << order_id << ", msg:" << msg_->DebugString();

    CancelOrderResponse* resp = order_id_to_response_map[order_id];

    bool succeeded = data["succeeded"].GetBool();

    if (!succeeded) {
      LOG(ERROR) << "[" << __func__ << "] cancel fail:" << JsonToString(data);
      auto exchange_error_code = GetExchangeErrorCode(data);
      auto exchange_error_msg = GetExchangeErrorMsg(data);
      auto error_code = GetErrorCode(exchange_error_code.value());

      resp->set_error_code(error_code);
      resp->set_exchange_error_code(exchange_error_code.value());
      resp->set_exchange_error_msg(exchange_error_msg.value());
      continue;
    }

    resp->set_success(true);
    resp->set_exchange_order_id(order_id);
    resp->set_cancel_confirmed(true);
  }
}

void GateioParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsGateioErrMessage(document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestAccountBalance] Invalid data format.\n" << JsonToString(document);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto GateioParser::ParseBalance(const JsonValue& doc) -> AccountBalance {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Gateio");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }

  return acc_balance;
}

bool GateioParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  [
    {
      "currency": "ETH",
      "available": "968.8",
      "locked": "0"
    }
  ]
  */
  for (const auto& data : doc.GetArray()) {
    CurrencyBalance balance;
    balance.set_currency_native(data["currency"].GetString());
    double avail = stod(data["available"].GetString());
    double hold = stod(data["locked"].GetString());
    balance.set_available(avail);
    balance.set_hold(hold);
    balance.set_total(avail + hold);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

void GateioParser::ParseRestOrder(
    const JsonValue& data,
    ProductOrderElement* product_order_element) {
  /*
  {
    "id": "12332324",
    "text": "t-123456",
    "create_time": "1548000000",
    "update_time": "1548000100",
    "currency_pair": "ETH_BTC",
    "status": "open",
    "type": "limit",
    "account": "spot",
    "side": "buy",
    "amount": "1",
    "price": "5.00032",
    "time_in_force": "gtc",
    "left": "0.5",
    "filled_total": "2.50016",
    "fee": "0.005",
    "fee_currency": "ETH",
    "point_fee": "0",
    "gt_fee": "0",
    "gt_discount": false,
    "rebated_fee": "0",
    "rebated_fee_currency": "BTC"
  }
  */
  product_order_element->set_exchange_type(ExchangeType::Gateio);
  product_order_element->set_market_type(MarketType::Spot);
  product_order_element->set_api_version("v4");
  product_order_element->set_exchange_order_id(data["id"].GetString());
  product_order_element->set_symbol(data["currency_pair"].GetString());
  product_order_element->set_price(stod(data["price"].GetString()));
  product_order_element->set_qty(stod(data["amount"].GetString()));
  product_order_element->set_proc_order_id(GetProcOrderId(data));
  product_order_element->set_side(GetOrderSide(data["side"].GetString()));
  product_order_element->set_order_type(GetOrderType(data["type"].GetString()));
  product_order_element->set_timestamp(msg_->timestamp);
  GetOrderStatus(data, product_order_element);
}

void GateioParser::ParseRestOpenOrdersInfo() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestOpenOrdersInfo] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsGateioErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestOpenOrdersInfo] Error message.\n" << JsonToString(doc);
    return;
  }

  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseRestOpenOrdersInfo] Invalid data format.\n" << JsonToString(doc);
    return;
  }

  ProductOrderBundle* product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();

  std::vector<int> totals;
  bool product_order_bundle_found = false;
  for (const auto& data : doc.GetArray()) {
    if (!data.HasMember("currency_pair")) {
      continue;
    }
    std::string native_symbol = data["currency_pair"].GetString();
    if (!symcache_->has_product_native(native_symbol)) {
      continue;
    }
    if (!data.HasMember("orders") || !data["orders"].IsArray() ||
        (data["orders"].GetArray().Size() <= 0)) {
      continue;
    }
    if (data.HasMember("total")) {
      totals.emplace_back(std::stoi(data["total"].GetString()));
    }
    for (const auto& info : data["orders"].GetArray()) {
      auto order = product_order_bundle->add_each_order();
      order->Clear();
      ParseRestOrder(info, order);
    }
    product_order_bundle_found = true;
  }

  auto max = std::max_element(totals.begin(), totals.end());
  if (max != totals.end() && *max > 100) {
    int total = *max;
    mb_->PublishEvent("gateio_spot/query_open_orders", &total);
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void GateioParser::ParseRestOpenOrdersInfoV2() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestOpenOrdersInfo] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsGateioErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestOpenOrdersInfo] Error message.\n" << JsonToString(doc);
    return;
  }

  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseRestOpenOrdersInfo] Invalid data format.\n" << JsonToString(doc);
    return;
  }

  ProductOrderBundle* product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();

  bool product_order_bundle_found = false;
  for (const auto& data : doc.GetArray()) {
    if (!data.HasMember("currency_pair")) {
      continue;
    }
    std::string native_symbol = data["currency_pair"].GetString();
    if (!symcache_->has_product_native(native_symbol)) {
      continue;
    }
    if (!data.HasMember("orders") || !data["orders"].IsArray() ||
        (data["orders"].GetArray().Size() <= 0)) {
      continue;
    }
    for (const auto& info : data["orders"].GetArray()) {
      auto order = product_order_bundle->add_each_order();
      order->Clear();
      ParseRestOrder(info, order);
    }
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void GateioParser::ParseRestHistOrdersInfo() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestHistOrdersInfo] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsGateioErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestHistOrdersInfo] Error message.\n" << JsonToString(doc);
    return;
  }

  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseRestHistOrdersInfo] Invalid data format.\n" << JsonToString(doc);
    return;
  }

  ProductOrderBundle* product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& data : doc.GetArray()) {
    auto order = product_order_bundle->add_each_order();
    order->Clear();
    ParseRestOrder(data, order);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void GateioParser::ParseRestFillInfo(
    const JsonValue& data,
    ProductFillElement* product_fill_element) {
  /*
  {
    "id": "1232893232",
    "create_time": "1548000000",
    "create_time_ms": "1548000000123.456",
    "order_id": "4128442423",
    "side": "buy",
    "role": "maker",
    "amount": "0.15",
    "price": "0.03",
    "fee": "0.0005",
    "fee_currency": "ETH",
    "point_fee": "0",
    "gt_fee": "0"
  }
  */
  product_fill_element->set_symbol(data["currency_pair"].GetString());
  product_fill_element->set_fill_timestamp(msg_->timestamp);
  product_fill_element->set_exchange_order_id(data["order_id"].GetString());
  product_fill_element->set_price(stod(data["price"].GetString()));
  product_fill_element->set_qty(stod(data["amount"].GetString()));
  product_fill_element->set_side(GetOrderSide(data["side"].GetString()));
  product_fill_element->set_fill_id(data["id"].GetString());
  product_fill_element->set_fee(stod(data["fee"].GetString()));
  product_fill_element->set_fee_currency(data["fee_currency"].GetString());
  std::string role = data["role"].GetString();
  if (role == "maker") {
    product_fill_element->set_fill_type(FillType::MAKER_FILL_TYPE);
  } else if (role == "taker") {
    product_fill_element->set_fill_type(FillType::TAKER_FILL_TYPE);
  } else {
    product_fill_element->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
  }
}

void GateioParser::ParseRestFillInfoList() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsGateioErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Error message.\n" << JsonToString(doc);
    return;
  }

  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseRestFillInfoList] Invalid data format.\n" << JsonToString(doc);
    return;
  }

  ProductFillBundle* product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : doc.GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    fill->Clear();
    ParseRestFillInfo(data, fill);
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void GateioParser::ParseWsOrderInfoList(const JsonDocument& document) {
  /*
  {
    "time": 1605175506,
    "channel": "spot.orders",
    "event": "update",
    "result": [
      {
        "id": "30784435",
        "user": 123456,
        "text": "t-abc",
        "create_time": "1605175506",
        "create_time_ms": "1605175506123",
        "update_time": "1605175506",
        "update_time_ms": "1605175506123",
        "event": "put",
        "currency_pair": "BTC_USDT",
        "type": "limit",
        "account": "spot",
        "side": "sell",
        "amount": "1",
        "price": "10001",
        "time_in_force": "gtc",
        "left": "1",
        "filled_total": "0",
        "fee": "0",
        "fee_currency": "USDT",
        "point_fee": "0",
        "gt_fee": "0",
        "gt_discount": true,
        "rebated_fee": "0",
        "rebated_fee_currency": "USDT"
      }
    ]
  }
  */
  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& data : document["result"].GetArray()) {
    auto order = product_order_bundle->add_each_order();
    order->set_exchange_type(ExchangeType::Gateio);
    order->set_market_type(MarketType::Spot);
    order->set_api_version("v4");
    order->set_exchange_order_id(data["id"].GetString());
    order->set_symbol(data["currency_pair"].GetString());
    order->set_price(stod(data["price"].GetString()));
    order->set_qty(stod(data["amount"].GetString()));
    order->set_proc_order_id(GetProcOrderId(data));
    order->set_side(GetOrderSide(data["side"].GetString()));
    order->set_order_type(GetOrderType(data["type"].GetString()));
    order->set_timestamp(msg_->timestamp);
    SetOrderStatus(data, order);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void GateioParser::ParseWsTradeInfoList(const JsonDocument& document) {
  /*
  {
    "time": 1605176741,
    "channel": "spot.usertrades",
    "event": "update",
    "result": [
      {
        "id": 5736713,
        "user_id": 1000001,
        "order_id": "30784428",
        "currency_pair": "BTC_USDT",
        "create_time": 1605176741,
        "create_time_ms": "1605176741123.456",
        "side": "sell",
        "amount": "1.00000000",
        "role": "taker",
        "price": "10000.00000000",
        "fee": "0.00200000000000",
        "point_fee": "0",
        "gt_fee": "0",
        "text": "apiv4"
      }
    ]
  }
  */
  ProductFillBundle* product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : document["result"].GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(data["currency_pair"].GetString());
    fill->set_fill_timestamp(msg_->timestamp);
    fill->set_exchange_order_id(data["order_id"].GetString());
    fill->set_price(stod(data["price"].GetString()));
    fill->set_qty(stod(data["amount"].GetString()));
    fill->set_side(GetOrderSide(data["side"].GetString()));
    fill->set_fill_id(data["id"].GetString());
    fill->set_fee(stod(data["fee"].GetString()));
    fill->set_fee_currency(data["fee_currency"].GetString());
    std::string role = data["role"].GetString();
    if (role == "maker") {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else if (role == "taker") {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    }
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void GateioParser::ParseWsBalanceInfoList(const JsonDocument& document) {
  /*
  {
    "time": 1605248616,
    "channel": "spot.balances",
    "event": "update",
    "result": [
      {
        "timestamp": "1605248616",
        "timestamp_ms": "1605248616123",
        "user": "1000001",
        "currency": "USDT",
        "change": "100",
        "total": "1032951.325075926",
        "available": "1022943.325075926"
      }
    ]
  }
  */
  auto acc_balance = result_.mutable_account_balance();
  acc_balance->Clear();
  acc_balance->set_exchange("Gateio");
  acc_balance->set_market_type("Spot");
  bool acc_balance_found = false;
  for (const auto& data : document["result"].GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("total") || !data.HasMember("available")) {
      continue;
    }
    if (!symcache_->has_currency_native(data["currency"].GetString())) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(data["currency"].GetString());
    auto* currency_balance = acc_balance->add_each_balance();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    auto total = stod(data["total"].GetString());
    auto avail = stod(data["available"].GetString());
    currency_balance->set_total(total);
    currency_balance->set_available(avail);
    currency_balance->set_hold(total - avail);
    acc_balance_found = true;
  }

  if (acc_balance_found) {
    result_.set_type(MsgType::ACCOUNT_BALANCE);
  }
}

void GateioParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsGateioErrMessage(document)) {
    LOG(ERROR) << "[ParseWsMessage] Error message.\n" << JsonToString(document);
    return;
  }

  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  if (IsOrderUpdate(document)) {
    ParseWsOrderInfoList(document);
  } else if (IsTradeUpdate(document)) {
    ParseWsTradeInfoList(document);
  } else if (IsBalanceUpdate(document)) {
    ParseWsBalanceInfoList(document);
  }
}

auto GateioParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }
  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::MULTI_SUBMIT_RESPONSE:
      ParseBatchSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      ParseBatchCancelResponse();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrdersInfo();
      break;
    case OrderSubTopicId::REST_ALL_ORDERS:  // alias of REST_OPEN_ORDERS
      ParseRestOpenOrdersInfoV2();
      break;
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseRestHistOrdersInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case GateioOrderSubTopicId::REST_DEPOSIT:
      ParseRestDeposit();
      break;
    case GateioOrderSubTopicId::REST_WITHDRAWAL:
      ParseRestWithdraw();
      break;
    case GateioOrderSubTopicId::REST_TRANSFERS:
      ParseRestTransfers();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::gateio::order_v4
